#define ZCORE_SOURCE
#include "TextProgressBar.hpp"
#include "Log.hpp"
#include "StringPrintf.hpp"
#include "../Math/Math.hpp"

// Mimic Google progress bar behavior

namespace zzz {
TextProgressBar::TextProgressBar(const string &msg, bool active_mode, Style style)
:msg_(msg), active_mode_(active_mode), max_(0), min_(0), value_(0), 
running_char_count_(0), style_(style), update_rate_(1), started_(false), delay_start_(0) {
  SetNormalChar('.', 'Z');
  SetZChar(".zZ");
  console_width_ = 79;
}

void TextProgressBar::SetActiveMode(bool mode) {
  active_mode_ = mode;
}

void TextProgressBar::SetMaximum(int x) {
  max_ = x;
}

void TextProgressBar::SetMinimum(int x) {
  min_ = x;
}

void TextProgressBar::SetValue(int x) {
  value_ = x;
}

void TextProgressBar::SetNormalChar(char blank, char fill) {
  blank_=blank;
  fill_=fill;
}

void TextProgressBar::Start() {
  if (!CheckDelayStart())
    return;
  ZCHECK_FALSE(started_)<<"The progress bar is already started!";
  static int ETA_length = strlen(" ETA 00:00:00");
  static int percent_length = strlen(" 100%");
  static int PST_length = strlen(" PST 00:00:00");

  running_char_count_=0;
  if (!active_mode_) {
    int msg_length = msg_.size() + 1;
    if (console_width_ - ETA_length - msg_length - percent_length < 7) {
      msg_length = console_width_ - ETA_length - percent_length - 7;
      msg_.assign(msg_.begin(), msg_.begin()+msg_length-3);
      msg_+="...";
    }
    bar_length_ = console_width_ - ETA_length - msg_length - percent_length - 2;
    value_ = min_;
    last_timer_.Restart();
    last_timer_count_=1;
    percentage_ = 0;
    if (style_ == STYLE_NORMAL) {
      // msg %XX  [ZZZZZZZZZ/__________] ETA 00:00:00
      // msg %100 [ZZZZZZZZZZZZZZZZZZZZ] ALL 01:01:01
      bar_.assign(bar_length_, blank_);
      ZLOG(ZVERBOSE_PROGRESS_BAR)<<StringPrintf("%s  %2d%% [%s] ETA %s", msg_.c_str(), 0, bar_.c_str(), SecondsToTime(0).c_str());
    } else if (style_ == STYLE_Z) {
      // msg %XX  [z__ZzZ_zzz_____zZ] ETA 00:00:00
      // msg %100 [ZZZZZZZZZZZZZZZZZ] ETA 01:23:45
      update_order_.reserve(bar_length_*(zstyle_char_.length()-1));
      for (zuint j=0; j<zstyle_char_.length()-1; j++) for (int i=0; i<bar_length_; i++) 
        update_order_.push_back(i);
      random_shuffle(update_order_.begin(), update_order_.end());
      next_update_=0;
      bar_char_count_.assign(bar_length_, 0);
      bar_.assign(bar_length_, zstyle_char_[0]);
      ZLOG(ZVERBOSE_PROGRESS_BAR)<<StringPrintf("%s  %2d%% [%s] ETA %s", msg_.c_str(), 0, bar_.c_str(), SecondsToTime(0).c_str());
    }
  } else {
    // msg [_____ZZZZZZZ__________] PST 00:00:00
    int msg_length = msg_.size() + 1;
    if (console_width_ - PST_length - msg_length < 7)
    {
      msg_length = console_width_ - PST_length - 7;
      msg_.assign(msg_.begin(), msg_.begin()+msg_length-3);
      msg_+="...";
    }
    bar_length_ = console_width_ - PST_length - msg_length - 2;
    actbar_length_ = Min(bar_length_ / 3, 20);
    actbar_pos_ = 0;
    bar_.assign(actbar_length_, fill_);
    bar_.append(bar_length_ - actbar_length_, blank_);
    ZLOG(ZVERBOSE_PROGRESS_BAR)<<msg_<<" ["<<bar_<<"] PST "<<SecondsToTime(0);
  }
  started_=true;
}

void TextProgressBar::End(bool clear) {
  if (!started_)
    return;
  timer_.Pause();
  if (clear) { 
    Clear();
  } else {
    if (!active_mode_)
      Show(true);
    else
      ShowActive(true);
    ZLOG(ZVERBOSE_PROGRESS_BAR)<<"\n";
  }
}

void TextProgressBar::Update(int value) {
  ZCHECK_FALSE(active_mode_)<<"Update() CANNOT be only called in Active Mode";
  if (!CheckDelayStart())
    return;
  if (!started_)
    Start();
  value_ = Clamp(min_, value, max_);
  Show(false);
}

void TextProgressBar::Pulse() {
  ZCHECK(active_mode_)<<"Pulse() can be only called in Active Mode";
  if (!CheckDelayStart())
    return;
  if (!started_)
    Start();
  ShowActive(false);
}

void TextProgressBar::Show(bool end) {
  double per = Clamp<double>(0.0, double(value_ - min_) / (max_ - min_), 1.0);
  // Only update when 1 sec past or percentage increased by 1
  if (!end && int(per) <= percentage_ && last_timer_.Elapsed() < update_rate_) {
    last_timer_count_++;
    return;
  }
  percentage_ = static_cast<int>(per * 100);
  running_char_count_++;

  Clear();
  // Prepare bar
  if (!end && per != 1.0) {
    // Running char.
    static char running_char[]="|/-\\";
    running_char_count_%=4;
    // Estimate time after
    double est = timer_.Elapsed() / value_ * (max_ - value_);
    if (style_ == STYLE_NORMAL) {
      int fill_length = per * bar_length_;
      bar_.assign(fill_length, fill_);
      if (fill_length < bar_length_) {
        bar_+=running_char[running_char_count_];
        bar_.append(bar_length_ - fill_length - 1, blank_);
      } 
      ZLOG(ZVERBOSE_PROGRESS_BAR)<<StringPrintf("%s %3d%% [%s] ETA %s", msg_.c_str(), percentage_, bar_.c_str(), SecondsToTime(est).c_str());
    } else if (style_ == STYLE_Z) {
      running_char_count_%=2;
      int l = per * bar_length_ * (zstyle_char_.length()-1);
      // Fixed part
      for (int i=next_update_; i<l; i++) {
        int update_pos = update_order_[i];
        bar_char_count_[update_pos]++;
        bar_[update_pos]= zstyle_char_[bar_char_count_[update_pos]];
      }
      next_update_ = l;
      // Running part
      if (next_update_ < bar_length_ * (zstyle_char_.length()-1)) {
        int update_pos = update_order_[next_update_];
        bar_[update_pos]= zstyle_char_[bar_char_count_[update_pos]+running_char_count_];
      }
      ZLOG(ZVERBOSE_PROGRESS_BAR)<<StringPrintf("%s %3d%% [%s] ETA %s", msg_.c_str(), percentage_, bar_.c_str(), SecondsToTime(est).c_str());
    }
  } else {
    // All time
    double all = timer_.Elapsed();
    if (style_ == STYLE_NORMAL)
      bar_.assign(bar_length_, fill_);
    else if (style_ == STYLE_Z)
      bar_.assign(bar_length_, zstyle_char_.back());
    // Draw.
    ZLOG(ZVERBOSE_PROGRESS_BAR)<<msg_<<" 100% ["<<bar_<<"] ALL "<<SecondsToTime(all);
  }
  last_timer_.Restart();
  last_timer_count_ = 1;
}

void TextProgressBar::ShowActive(bool end) {
  // Only update when 1 sec past
  if (!end && last_timer_.Elapsed() < update_rate_)
    return;
  else
    last_timer_.Restart();

  Clear();
  // Prepare bar
  if (!end) {
    actbar_pos_++;
    actbar_pos_ %= bar_length_;

    if (bar_length_ - actbar_pos_ > actbar_length_) {
      bar_.assign(actbar_pos_, blank_);
      bar_.append(actbar_length_, fill_);
      bar_.append(bar_length_ - actbar_pos_ - actbar_length_, blank_);
    } else {
      bar_.assign(actbar_length_ - (bar_length_ - actbar_pos_), fill_);
      bar_.append(bar_length_ - actbar_length_, blank_);
      bar_.append(bar_length_ - actbar_pos_, fill_);
    }
    // Draw.
    ZLOG(ZVERBOSE_PROGRESS_BAR)<<msg_<<" ["<<bar_<<"] PST "<<SecondsToTime(timer_.Elapsed());
  } else {
    // All time
    double all = timer_.Elapsed();
    bar_.assign(bar_length_, fill_);
    // Draw.
    ZLOG(ZVERBOSE_PROGRESS_BAR)<<msg_<<" ["<<bar_<<"] ALL "<<SecondsToTime(timer_.Elapsed());
  }
}

void TextProgressBar::Clear() {
  ZLOG(ZVERBOSE_PROGRESS_BAR)<<'\r';
}

string TextProgressBar::SecondsToTime(double sec) {
  int seconds = static_cast<int>(sec);
  int s = seconds % 60;
  seconds = (seconds - s) / 60;
  int m = seconds % 60;
  seconds = (seconds - m) / 60;
  int h = seconds;
  if (h>99) 
    return StringPrintf("**:**:**", h, m, s);
  else
    return StringPrintf("%02d:%02d:%02d", h, m, s);
}

void TextProgressBar::DeltaUpdate(int x/*=1*/) {
  if (!CheckDelayStart())
    return;
  if (!started_)
    Start();
  Update(value_+x);
}

void TextProgressBar::SetZChar(const string &zstyle_char) {
  zstyle_char_=zstyle_char;
}

void TextProgressBar::SetUpdateRate(double rate) {
  update_rate_ = rate;
}

void TextProgressBar::SetDelayStart(double sec) {
  delay_start_ = sec;
}

bool TextProgressBar::CheckDelayStart() {
  static bool firstrun = true;
  if (firstrun) {
    timer_.Restart();
    firstrun = false;
  }
  if (delay_start_ > 0 && timer_.Elapsed() < delay_start_)
    return false;
  return true;
}

}  // namespace zzz
